package api

import (
	"encoding/hex"
	"io"
	"io/ioutil"
	"moss/conf"
	"moss/fs"
	"moss/meta"
	"moss/proto"
	"moss/status"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"
)

func InitiateMultipartUpload(accessKeyId, bucketName, objectName, permission string, userMeta, encryption string) (*proto.InitiateMultipartUploadResult, status.Status) {
	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	if len(encryption) != 0 && encryption != "AES256" {
		return nil, status.InvalidEncryptionAlgorithmError
	}

	bucketMeta, ok := meta.GetBucketInfo(bucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}

	if !checkPermission(bucketMeta.Permission, bucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return nil, status.AccessDenied
	}

	mu, ok := meta.AddMultipartUpload(bucketName, objectName, buildUploadId(), encryption, userMeta, permission)
	if !ok {
		return nil, status.InvalidArgument
	}

	entry := proto.InitiateMultipartUploadResult{
		Bucket:   mu.Bucket,
		Key:      mu.Key,
		UploadId: mu.UploadId}

	return &entry, status.Success
}

func UploadPart(requestId string, accessKeyId string, bucketName string, objectName string, partNumber string, uploadId string, document io.ReadCloser,
	contentLength int64, contentChecksum string) (*proto.Part, string, status.Status) {

	if contentLength <= 0 {
		return nil, "", status.MissingContentLength
	}

	uploader, found := meta.FindMultipartUpload(uploadId)
	if !found {
		return nil, "", status.NoSuchUpload
	}

	encryption := uploader.Encryption
	if len(encryption) != 0 && encryption != "AES256" {
		return nil, "", status.InvalidEncryptionAlgorithmError
	}

	if !validIdentifier(bucketName) {
		return nil, "", status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, "", status.InvalidObjectName
	}

	bucketInfo, found := meta.GetBucketInfo(bucketName)
	if !found {
		return nil, "", status.NoSuchBucket
	}

	if !checkPermission(bucketInfo.Permission, bucketInfo.OwnerAccessKeyId, accessKeyId, conf.AccessTypeWrite) {
		return nil, "", status.AccessDenied
	}

	partName := objectName + ".part" + partNumber
	partNumberInt, err := strconv.ParseInt(partNumber, 10, 64)
	if err != nil || partNumberInt > 10000 || partNumberInt < 1 {
		return nil, "", status.InvalidArgument
	}

	fileLocation := fs.GetObjectLocation(bucketName, partName, 0)
	rc := fs.WriteFile(fileLocation, document, contentLength, encryption)
	if rc != status.Success {
		return nil, "", rc
	}

	tag := fs.CalculateMD5(fileLocation, encryption)
	if len(tag) == 0 {
		return nil, "", status.InternalError
	}

	if len(contentChecksum) > 0 && contentChecksum != fs.MD5Hex2Base64(tag) {
		fs.RemoveFile(fileLocation)
		return nil, "", status.InvalidDigest
	}

	lastModified := time.Now().Format(time.RFC3339)
	meta.SetUploadPart(uploadId, bucketName, partName, partNumberInt, tag, contentLength, fileLocation, lastModified)
	meta.IncreaseMultipartUploadParts(uploadId)

	entry := proto.Part{
		PartNumber:   partNumber,
		ETag:         tag,
		LastModified: lastModified,
		Size:         string(contentLength)}

	return &entry, encryption, status.Success
}

func UploadPartCopy(requestId, accessKeyId, bucketName, objectName, partNumber, uploadId string, srcObjectMeta *meta.Object, offset int64,
	length int64, contentChecksum string) (*proto.CopyPartResult, status.Status) {

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	filename := fs.GetObjectLocation(srcObjectMeta.BucketName, srcObjectMeta.Key, 0)
	doc := fs.SafeOpenFile(filename, srcObjectMeta.Size)
	tempFile, err := ioutil.TempFile(conf.Config.Path.Temp, "tmp-")
	if err != nil {
		return nil, status.InternalError
	}

	defer tempFile.Close()

	_, err = doc.Seek(offset, io.SeekStart)
	if err != nil {
		return nil, status.InternalError
	}

	var srcHandler io.Reader = doc
	if srcObjectMeta.Encryption == "AES256" {
		privateKey, _ := hex.DecodeString(conf.Config.Security.AesPrivateKey)
		srcHandler = fs.EncryptReader(doc, privateKey)
	}

	_, err = io.CopyN(tempFile, srcHandler, length)
	if err != nil {
		return nil, status.InternalError
	}

	document, _ := os.Open(tempFile.Name())
	part, _, rc := UploadPart(requestId, accessKeyId, bucketName, objectName, partNumber, uploadId, document,
		length, contentChecksum)

	os.Remove(tempFile.Name())

	if rc != status.Success {
		return nil, rc
	}

	entry := &proto.CopyPartResult{
		LastModified: time.Now().Format(time.RFC3339),
		ETag:         part.ETag}

	return entry, status.Success
}

func CompleteMultipartUpload(requestId, accessKeyId, bucketName, objectName, uploadId string, parts []proto.Part) (*proto.CompleteMultipartUploadResult, status.Status) {
	var lastNumber int64 = 10000
	var writeParts []meta.UploadPart

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	if len(meta.ListUploadPart(uploadId, 0)) != len(parts) {
		return nil, status.InvalidPartOrder
	}
	sort.Sort(fs.PartSort(parts))

	for _, v := range parts {
		partNumber, _ := strconv.ParseInt(v.PartNumber, 10, 64)
		part, ok := meta.GetUploadPart(uploadId, partNumber)
		if !ok {
			return nil, status.InvalidPart
		}

		if part.Size < 100 {
			lastNumber = part.PartNumber
		}

		if part.PartNumber > lastNumber {
			return nil, status.InvalidPart
		}

		if v.ETag != part.ETag {
			return nil, status.InvalidPart
		}

		writeParts = append(writeParts, *part)
	}

	fileLocation := fs.GetObjectLocation(bucketName, objectName, 0)
	var length int64 = 0
	mu, _ := meta.FindMultipartUpload(uploadId)
	tempFile, _ := ioutil.TempFile(conf.Config.Path.Temp, "tmp-complete-")
	for _, v := range writeParts {
		doc, _ := os.Open(v.FileLocation)
		e := fs.AppendFile(tempFile.Name(), doc, length, v.Size, mu.Encryption)
		if e != status.Success {
			return nil, e
		}
		length = length + v.Size
	}

	tempFile.Seek(0, 0)
	tag, rc := PutObject(requestId, accessKeyId, bucketName, objectName, tempFile, length,
		"", mu.Encryption, mu.Permission, mu.UserMeta)
	if rc != status.Success {
		return nil, rc
	}

	tempFile.Close()
	os.Remove(tempFile.Name())

	rc = AbortMultipartUpload(bucketName, objectName, uploadId)
	if rc != status.Success {
		return nil, rc
	}

	cmur := &proto.CompleteMultipartUploadResult{
		Location: fileLocation,
		ETag:     tag,
		Bucket:   bucketName,
		Key:      objectName}

	return cmur, status.Success
}

func ListMultipartUpload(requestId, accessKeyId, bucketName, delimiter, marker, prefix,
	uploadIdMarker string, maxUploads int) (*proto.ListMultipartUploadsResult, status.Status) {
	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}

	bucketMeta, ok := meta.GetBucketInfo(bucketName)
	if !ok {
		return nil, status.NoSuchBucket
	}
	if !checkPermission(bucketMeta.Permission, bucketMeta.OwnerAccessKeyId, accessKeyId, conf.AccessTypeRead) {
		return nil, status.AccessDenied
	}
	if maxUploads < 0 || maxUploads > 1000 {
		return nil, status.InvalidArgument
	}
	if len(prefix) >= conf.ParameterMaxLength {
		return nil, status.InvalidArgument
	}
	if len(marker) >= conf.ParameterMaxLength {
		return nil, status.InvalidArgument
	}

	allMu := meta.ListMultipartUpload(bucketName, prefix)

	var lmurInfo proto.ListMultipartUploadsResult
	var lmu []proto.Upload
	insertedKeys := 0

	lmurInfo.Bucket = bucketName
	lmurInfo.KeyMarker = marker
	lmurInfo.UploadIdMarker = uploadIdMarker
	lmurInfo.Delimiter = delimiter
	lmurInfo.Prefix = prefix
	lmurInfo.MaxUploads = strconv.Itoa(maxUploads)

	if allMu == nil {
		return &lmurInfo, status.Success
	}

	for _, v := range allMu {
		if len(marker) != 0 && v.Key < marker {
			continue
		}

		if insertedKeys == maxUploads {
			lmurInfo.NextKeyMarker = v.Key
			lmurInfo.NextUploadIdMarker = v.UploadId
			lmurInfo.IsTruncated = "true"
			break
		}

		mu := proto.Upload{
			Key:       v.Key,
			UploadId:  v.UploadId,
			Initiated: v.Date}

		lmu = append(lmu, mu)

		keyWithoutPrefix := strings.TrimPrefix(v.Key, prefix)
		if strings.Contains(keyWithoutPrefix, "/") && delimiter == "/" {
			// Key is a directory (conf prefix)
			thisPrefix := prefix + keyWithoutPrefix[:strings.Index(keyWithoutPrefix, "/")]
			lmurInfo.CommonPrefixes = append(lmurInfo.CommonPrefixes, thisPrefix)
		}

		insertedKeys++
	}

	lmurInfo.Upload = lmu
	return &lmurInfo, status.Success
}

func ListParts(requestId string, accessKeyId string, uploadId, bucketName string, objectName string,
	marker string, maxParts int) (*proto.ListPartsResult, status.Status) {
	var uploadParts []meta.UploadPart
	var part proto.Part

	if !validIdentifier(bucketName) {
		return nil, status.InvalidBucketName
	}
	if !validIdentifier(objectName) {
		return nil, status.InvalidObjectName
	}

	markerInt64, _ := strconv.ParseInt(marker, 10, 64)
	uploadParts = meta.ListUploadPart(uploadId, markerInt64)

	partList := &proto.ListPartsResult{
		Bucket:               bucketName,
		Key:                  objectName,
		UploadId:             uploadId,
		NextPartNumberMarker: "",
		MaxParts:             strconv.Itoa(maxParts),
		IsTruncated:          "false"}

	insertedKeys := 0

	for _, v := range uploadParts {
		if insertedKeys == maxParts {
			partList.NextPartNumberMarker = strconv.FormatInt(v.PartNumber, 10)
			partList.IsTruncated = "true"
			break
		}
		part.ETag = v.ETag
		part.LastModified = v.LastModified
		part.PartNumber = strconv.FormatInt(v.PartNumber, 10)
		part.Size = strconv.FormatInt(v.Size, 10)

		partList.Part = append(partList.Part, part)
		insertedKeys++
	}
	return partList, status.Success
}

func AbortMultipartUpload(bucketName string, objectName string, uploadId string) status.Status {
	if !validIdentifier(bucketName) {
		return status.InvalidBucketName
	}

	if !validIdentifier(objectName) {
		return status.InvalidObjectName
	}

	_, ok := meta.GetObjectInfo(bucketName, objectName)
	if !ok {
		return status.NoSuchUpload
	}

	_, ok = meta.FindMultipartUpload(uploadId)
	if !ok {
		return status.NoSuchUpload
	}

	meta.DeleteMultipartUpload(uploadId)

	uploadPart := meta.ListUploadPart(uploadId, 0)
	meta.DeleteUpload(uploadId)
	for _, v := range uploadPart {
		fs.RemoveFile(v.FileLocation)
	}

	return status.Success
}
